/*******************************************************************************
 * Copyright (c) Dec 20, 2015 @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>.
 * All rights reserved.
 *
 * Contributors:
 *     <a href="mailto:iffiff1@gmail.com">Tyler Chen</a> - initial API and implementation
 ******************************************************************************/
package org.iff.util;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.lang.reflect.Constructor;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.*;

/**
 * a zip helper.
 *
 * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
 * @since Dec 20, 2015
 */
public class ZipHelper {
    public static final int BUFFER = 1024 * 10;
    private static Constructor<?> zipInputStreamJdk7 = null;
    private static Constructor<?> zipInputStreamJdk6 = null;
    private static Constructor<?> zipOutputStreamJdk7 = null;
    private static Constructor<?> zipOutputStreamJdk6 = null;

    /**
     * read input stream as zip input stream.
     *
     * @param is
     * @param charset
     * @return
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static ZipInputStream getZipInputStream(InputStream is, Charset charset) {
        if (zipInputStreamJdk7 == null && zipInputStreamJdk6 == null) {
            zipInputStreamJdk7 = getConstructor(ZipInputStream.class, InputStream.class.getName(),
                    Charset.class.getName());
            zipInputStreamJdk6 = getConstructor(ZipInputStream.class, InputStream.class.getName());
        }
        try {
            return (ZipInputStream) (zipInputStreamJdk7 == null ? zipInputStreamJdk6.newInstance(is)
                    : zipInputStreamJdk7.newInstance(is, charset));
        } catch (Exception e) {
            throw new RuntimeException("Create ZipInputStream error!", e);
        }
    }

    /**
     * read output stream as zip output stream.
     *
     * @param os
     * @param charset
     * @return
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static ZipOutputStream getZipOutputStream(OutputStream os, Charset charset) {
        if (zipOutputStreamJdk7 == null && zipOutputStreamJdk6 == null) {
            zipOutputStreamJdk7 = getConstructor(ZipOutputStream.class, OutputStream.class.getName(),
                    Charset.class.getName());
            zipOutputStreamJdk6 = getConstructor(ZipOutputStream.class, OutputStream.class.getName());
        }
        try {
            return (ZipOutputStream) (zipOutputStreamJdk7 == null ? zipOutputStreamJdk6.newInstance(os)
                    : zipOutputStreamJdk7.newInstance(os, charset));
        } catch (Exception e) {
            throw new RuntimeException("Create ZipOutputStream error!", e);
        }
    }


    /**
     * read zip file from path, and unzip to dir.
     *
     * @param zipFile
     * @param dir
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static void unzip(InputStream zipFile, File dir) {
        BufferedOutputStream dest = null;
        ZipInputStream zis = null;
        try {
            File parent = dir;
            parent.mkdirs();
            CheckedInputStream checksum = new CheckedInputStream(zipFile, new Adler32());
            zis = getZipInputStream(new BufferedInputStream(checksum), Charset.forName("UTF-8"));
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                //System.out.println("Extracting: " + entry.getName() + ", size: " + entry.getSize());
                int count;
                byte data[] = new byte[BUFFER];
                File file = new File(parent, entry.getName());
                file.getParentFile().mkdirs();
                if (entry.isDirectory()) {
                    continue;
                }
                try {
                    FileOutputStream fos = new FileOutputStream(file);
                    dest = new BufferedOutputStream(fos, BUFFER);
                    while ((count = zis.read(data, 0, BUFFER)) != -1) {
                        dest.write(data, 0, count);
                    }
                    dest.flush();
                } finally {
                    IOUtils.closeQuietly(dest);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("unzip error!", e);
        } finally {
            IOUtils.closeQuietly(zis);
        }
    }


    /**
     * read zip file from path, and unzip to dir.
     *
     * @param zipFile
     * @param dir
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static void unzip(String zipFile, String dir) {
        try {
            unzip(new FileInputStream(zipFile), new File(dir));
        } catch (Exception e) {
            throw new RuntimeException("unzip error!", e);
        }
    }

    public static byte[] getFile(String zipFile, String fileName) {
        ZipInputStream zis = null;
        try {
            FileInputStream fis = new FileInputStream(zipFile);
            CheckedInputStream checksum = new CheckedInputStream(fis, new Adler32());
            zis = getZipInputStream(new BufferedInputStream(checksum), Charset.forName("UTF-8"));
            return getFile(zis, fileName);
        } catch (Exception e) {
            throw new RuntimeException("Open zip file error.", e);
        } finally {
            IOUtils.closeQuietly(zis);
        }
    }

    public static ZipInputStream getZipInputStream(InputStream zipFile) {
        ZipInputStream zis = null;
        try {
            CheckedInputStream checksum = new CheckedInputStream(zipFile, new Adler32());
            zis = getZipInputStream(new BufferedInputStream(checksum), Charset.forName("UTF-8"));
            return zis;
        } catch (Exception e) {
            throw new RuntimeException("Open zip file error.", e);
        }
    }

    public static byte[] getFile(InputStream zipFile, String fileName) {
        ZipInputStream zis = null;
        try {
            CheckedInputStream checksum = new CheckedInputStream(zipFile, new Adler32());
            zis = getZipInputStream(new BufferedInputStream(checksum), Charset.forName("UTF-8"));
            return getFile(zis, fileName);
        } catch (Exception e) {
            throw new RuntimeException("Open zip file error.", e);
        }
    }

    public static byte[] getFile(ZipInputStream zis, String fileName) {
        fileName = !fileName.startsWith("/") ? ("/" + fileName) : fileName;
        try {
            ZipEntry entry = null;
            while ((entry = zis.getNextEntry()) != null) {
                String name = entry.getName();
                name = !name.startsWith("/") ? ("/" + name) : name;
                System.out.println("Extracting: " + name + ", size: " + entry.getSize());
                if (!fileName.equals(name)) {
                    continue;
                }
                BufferedOutputStream dest = null;
                int count;
                byte data[] = new byte[BUFFER];
                try {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
                    dest = new BufferedOutputStream(baos, BUFFER);
                    while ((count = zis.read(data, 0, BUFFER)) != -1) {
                        dest.write(data, 0, count);
                    }
                    dest.flush();
                    return baos.toByteArray();
                } finally {
                    IOUtils.closeQuietly(dest);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("unzip error!", e);
        }
        return null;
    }

    /**
     * read zip from file path, and unzip entry to a map.
     *
     * @param zipFile
     * @return
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static Map<String, byte[]> loadZip(String zipFile) {
        BufferedOutputStream dest = null;
        ZipInputStream zis = null;
        Map<String, byte[]> map = new LinkedHashMap<String, byte[]>();
        try {
            FileInputStream fis = new FileInputStream(zipFile);
            CheckedInputStream checksum = new CheckedInputStream(fis, new Adler32());
            zis = getZipInputStream(new BufferedInputStream(checksum), Charset.forName("UTF-8"));
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                System.out.println("Extracting: " + entry.getName() + ", size: " + entry.getSize());
                int count;
                byte data[] = new byte[BUFFER];
                try {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
                    dest = new BufferedOutputStream(baos, BUFFER);
                    while ((count = zis.read(data, 0, BUFFER)) != -1) {
                        dest.write(data, 0, count);
                    }
                    dest.flush();
                    map.put(entry.getName(), baos.toByteArray());
                } finally {
                    IOUtils.closeQuietly(dest);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("unzip error!", e);
        } finally {
            IOUtils.closeQuietly(zis);
        }
        return map;
    }

    /**
     * unzip an zip input stream to entry map.
     *
     * @param is
     * @return
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static Map<String, byte[]> loadZip(InputStream is) {
        BufferedOutputStream dest = null;
        ZipInputStream zis = null;
        Map<String, byte[]> map = new LinkedHashMap<String, byte[]>();
        try {
            CheckedInputStream checksum = new CheckedInputStream(is, new Adler32());
            zis = getZipInputStream(new BufferedInputStream(checksum), Charset.forName("UTF-8"));
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                System.out.println("Extracting: " + entry.getName() + ", size: " + entry.getSize());
                int count;
                byte data[] = new byte[BUFFER];
                try {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
                    dest = new BufferedOutputStream(baos, BUFFER);
                    while ((count = zis.read(data, 0, BUFFER)) != -1) {
                        dest.write(data, 0, count);
                    }
                    dest.flush();
                    map.put(entry.getName(), baos.toByteArray());
                } finally {
                    IOUtils.closeQuietly(dest);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("unzip error!", e);
        } finally {
            IOUtils.closeQuietly(zis);
        }
        return map;
    }

    /**
     * zip paths include sub dir and files to zip file with zip root folder.
     *
     * @param paths
     * @param zipFileName
     * @param rootFolderName
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static void zip(String[] paths, String zipFileName, String rootFolderName) {
        BufferedInputStream origin = null;
        ZipOutputStream out = null;
        try {
            if (paths == null || paths.length == 0) {
                System.out.println("No zip entry input, the paths is empty.");
                return;
            }
            FileOutputStream dest = new FileOutputStream(zipFileName);
            CheckedOutputStream checksum = new CheckedOutputStream(dest, new Adler32());
            out = getZipOutputStream(new BufferedOutputStream(checksum), Charset.forName("UTF-8"));
            //out.setMethod(ZipOutputStream.DEFLATED);
            byte data[] = new byte[BUFFER];
            for (String path : paths) {
                // get a list of files from current directory
                File pathFile = new File(path);
                String pathFileAsbPath = pathFile.getAbsolutePath();
                String folderName = "";
                Collection<File> files = new ArrayList<File>();
                {
                    if (pathFile.isDirectory()) {
                        files = FileUtils.listFiles(pathFile, FileFilterUtils.fileFileFilter(),
                                FileFilterUtils.directoryFileFilter());
                        folderName = pathFile.getName();
                    } else if (pathFile.isFile()) {
                        files.add(pathFile);
                        File parent = pathFile.getParentFile();
                        pathFileAsbPath = parent == null ? "" : parent.getAbsolutePath();
                        folderName = "";
                    }
                }

                for (File file : files) {
                    System.out.println("Adding: " + file);
                    try {
                        FileInputStream fi = new FileInputStream(file);
                        origin = new BufferedInputStream(fi, BUFFER);
                        String entryName = pathConcat(rootFolderName, folderName, file.getAbsolutePath().substring(pathFileAsbPath.length()));
                        entryName = StringUtils.removeStart(entryName, "/");
                        ZipEntry entry = new ZipEntry(entryName);
                        out.putNextEntry(entry);
                        int count;
                        while ((count = origin.read(data, 0, BUFFER)) != -1) {
                            out.write(data, 0, count);
                        }
                    } finally {
                        IOUtils.closeQuietly(origin);
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("zip error!", e);
        } finally {
            IOUtils.closeQuietly(out);
        }
    }

    /**
     * zip paths include sub dir and files to zip file without zip root folder.
     *
     * @param path
     * @param zipFileName
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since Jul 19, 2016
     */
    public static void zip(String path, String zipFileName) {
        if (StringUtils.isEmpty(path)) {
            System.out.println("No zip entry input, the paths is empty.");
            return;
        }
        File file = new File(path);
        zip(new String[]{path}, zipFileName, file.isDirectory() ? file.getName() : null);
    }

    static Constructor<?> getConstructor(Class clazz, String... parameterTypes) {
        try {
            if (parameterTypes == null || parameterTypes.length < 1) {
                try {
                    return clazz.getConstructor();
                } catch (Exception e) {
                    return null;
                }
            }
            Constructor<?>[] constructors = clazz.getConstructors();
            for (Constructor<?> c : constructors) {
                Class<?>[] types = c.getParameterTypes();
                if (types.length != parameterTypes.length) {
                    continue;
                }
                int i = 0;
                for (; i < types.length; i++) {
                    if (!types[i].getName().equals(parameterTypes[i])) {
                        break;
                    }
                }
                if (i == types.length) {
                    return c;
                }
            }
        } catch (Exception e) {
            System.out.println("[NoConstructorFound] className:" + clazz.getName() + ", parameterTypes:" + parameterTypes);
        }
        return null;
    }


    /**
     * concatenate strings with path separator. null value convert to blank string.
     *
     * @param paths
     * @return
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since 2015-2-6
     */
    static String pathConcat(String... paths) {
        if (paths == null || paths.length == 0) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0, j = paths.length - 1; i < paths.length; i++) {
            String path = paths[i];
            if (path == null) {
                path = "";
            }
            sb.append(path);
            if (i != j) {
                sb.append(getFileSeparator());
            }
        }
        return pathBuild(sb.toString(), getFileSeparator());
    }

    /**
     * <pre>
     * build the beautiful path.
     * example:
     * c:\a\\\\b\\\c   =>  c:\a\b\c
     * c:\a\/b\\\c   =>  c:\a\b\c
     * /a\/b\\\c   =>  /a/b/c
     * </pre>
     *
     * @param str
     * @param fileSeparator
     * @return
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since 2012-7-12
     */
    static String pathBuild(String str, String fileSeparator) {
        if (str == null || str.length() == 0) {
            return "";
        }
        String replace = fileSeparator;
        StringBuffer sb = new StringBuffer(str);
        {
            Matcher matcher = Pattern.compile("((\\\\+|/+)+)").matcher(str);
            int offset = 0;
            for (; matcher.find(); ) {
                int start = matcher.start(), end = matcher.end();
                sb.replace(Math.min(start + offset, sb.length()), Math.min(end + offset, sb.length()), replace);
                offset += replace.length() - (end - start);
            }
        }
        {
            Matcher matcher = Pattern.compile("((\\\\+|/+)+)").matcher(sb.toString());
            int offset = 0;
            for (; matcher.find(); ) {
                int start = matcher.start(), end = matcher.end();
                sb.replace(Math.min(start + offset, sb.length()), Math.min(end + offset, sb.length()), replace);
                offset += replace.length() - (end - start);
            }
        }
        return sb.toString();
    }

    /**
     * @return file or path separator
     * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
     * @since 2015-2-6
     */
    static String getFileSeparator() {
        return File.separator;
    }
}
